<?php
/**
 *
 *
 *
 */
abstract class Yz_Template_Abstract
    extends Yz_Class
    implements Yz_Template_Interface
{

    // redefine this in concrete template classes
    const TEMPLATE_TYPE = '';

    protected static $templatePath = array(
        self::TEMPLATE_PATH_GLOBAL,
        self::TEMPLATE_PATH_MODULE,
    );

    /** The template's name
     *  @var string
     */
    protected $name;

    /** The template's file name
     *  @var string
     */
    protected $fileName;

    /** The template file's owner class
     *  @var string
     */
    protected $ownerClass;

    /** The template's page title
     *  @var string
     */
    protected $title;

    /** The storage for assigned variables: link to __dict__
     *  @var array
     */
    #protected $__dict__ = array();
    protected $value;
    protected $values;

    /** Reference to the module
     *  @var Yz_Module_Interface
     */
    protected $module;

    /** Reference to the i18n
     *  @var Yz_I18N_Interface
     */
    protected $i18n;

    /** Meta tags for the template
     *  @var array
     */
    protected $meta = array(
        self::META_DESCRIPTION => '',
        self::META_KEYWORDS => '',
        );

    /** Scripts added via include
     *  @var array
     */
    protected $js = array();

    /** Styles added via includeCSS
     *  @var array
     */
    protected $css = array();

    /** Included forms added via includeForm
     *  @var array
     */
    protected $forms = array();

    /** The content once rendered, if render caching is enabled
     *
     *  A value of RENDER_CACHE_EMPTY means render caching is enabled, and template not rendered yet
     *  A value of RENDER_CACHE_NONE means render caching is disabled
     *  A string value is the content already rendered
     *  @var string|bool
     */
    protected $content = self::RENDER_CACHE_EMPTY;


    protected $myPath = array();

    /****************************
     *      Public methods      *
     ****************************/


    /**
     *
     */
    public function __construct( Yz_Module_Interface $module )
    {
        debug();
        $this->module = $module;
        $this->value =& $this->__dict__;
        $this->values =& $this->__dict__;
        $this->name = lcfirst($this->shortClassName());
    }


    /** render the template or get its content if already rendered
     *
     *  @return string
     */
    public function render()
    {
        debug();
        $c =& $this->content;
        if (self::RENDER_CACHE_NONE !== $c && self::RENDER_CACHE_EMPTY !== $c) {

            // return saved content
            return $c;
        }
        $r = $this->doRender();
        if (self::RENDER_CACHE_NONE !== $c) {

            // caching enabled => save rendered content
            $c = $r;
        }
        return $r;
    }

    /**
     * @return string
     */
    public function name()
    {
        return $this->name;
    }


    /**
     *  @return string
     */
    public function getFile()
    {
        $fn =& $this->fileName;
        if (null === $fn) {
            // pre-assign path and owner
            $this->ownerClass = $this;
            $this->myPath = static::$templatePath;
            $fn = $this->identifyFile();
        }
        return $fn;
    }

    /**
     *  @return string
     */
    public function getParentFile()
    {
        debug();
        return $this->identifyFile();
    }


    /** Include a javascript into the HEAD
     *  @param string|null  $src serves as unique id/name
     *  @param string|null  $text ignored if $src is not empty
     *  @return chainable
     *  @throw Yz_Exception_InvalidArgument if both src and text are empty
     */
    public function includeJS($src, $text=null)
    {
        debug();
        assert('is_string($src)');
        if (null !== $text)
            assert('is_string($text)');
        $this->js[$src] = $text;
        return $this;
    }


    /** Include a stylesheet into the HEAD as a LINK or STYLE tag
     *  @param string|null  $href serves as unique id/name
     *  @param string|null  $text ignored if $href is not empty
     *  @return chainable
     *  @throw Yz_Exception_InvalidArgument if both href and text are empty
     */
    public function includeCSS($href, $text=null)
    {
        assert('is_string($href)');
        if (null !== $text)
            assert('is_string($text)');
        $this->css[$href] = $text;
        return $this;
    }

    /**
     *  @return string
     */
    public function getTitle()
    {
        $title =& $this->title;
        if (null === $title) {
            if (array_key_exists('title', $this->__dict__))
                $title = $this->__dict__['title'];
            else
                $title = ':' . $this->shortClassName();

            $title = $this->translate($title);
            $values = $this->__dict__;
            $title = preg_replace_callback('~%\{([A-Za-z0-9._]+)}~',
                function($m) use ($values) {
                    $expr = str_replace( '.', "']['", $m[1] );
                    return eval("return isset(\$values['{$expr}']) ? \$values['{$expr}'] : false;") ?: $m[0];
                },
                $title
            );
        }
        return $title;
    }

    /**
     *  @return array
     */
    public function assignMeta($metaEntry, $content)
    {
        assert('is_string($metaEntry)');
        assert('is_string($content)');
        $this->meta[$metaEntry] = $content;
        return $this;
    }


    /** Include a form
     *  @param Yz_Form_Interface $form
     *  @return chainable
     */
    public function includeForm(Yz_Form_Interface $form)
    {
        $n = strtolower($form->getName());
        debug($n);
        $this->forms[$n] = $form;
        return $this;
    }


    /** Render a form by name
     *  @param string $name
     *  @return chainable
     */
    public function renderForm($formName=null)
    {
        debug();
        if (null !== $formName) {
            assert('is_string($formName)');
            $n = strtolower($formName);
            if (! array_key_exists($n, $this->forms))
                throw new Yz_Template_Exception("Form {$n} not found");
            $form = $this->forms[$n];
        } else {
            if (! $this->forms)
                throw new Yz_Exception('No forms included yet');
            $form = current($this->forms);
        }
        return $this->doRenderForm($form);
    }


    /** Render a template: either a template object or creating a template by name and module name
     *  @param Yz_Template_Interface|string $template
     *  @param string|null $moduleName
     *  @return string rendered template
     */
    public function renderTemplate($template, $moduleName=null)
    {
        debug();
        if ($template instanceof Yz_Template_Interface) {
        } else  {
            if (! is_string($template))
                throw new Yz_Template_Exception("Invalid template specification");
            if (null === $moduleName)
                $module = $this->getModule();
            else
                $module = Yz::selectModule($moduleName);
            $class = $module->resolveLibrary('Template', $template);
            $template = new $class($module);
        }
        return $this->doRenderTemplate($template);
    }

    /**
     *
     *  @todo real translate for non-empty lang and parameters
     */
    public function translate( $msg )
    {
        return $this->getI18N()->translate($msg);
    }

    /**
     *
     *  @todo real translate for non-empty lang and parameters
     */
    public function translateWithParams( $msg, $args )
    {
        return $this->getI18N()->translateWithParams($msg, $args);
    }

    /****************************
     *    Protected methods     *
     ***************************/

    /** defaultGetter() : override the Yz_Class
     *  @return mixed | null if index not in list
     */
    protected function defaultGetter( $index )
    {
        if ( ! $this->indexExists($index) )
            return null;
        return $this->__dict__[$index];
    }

    /** defaultSetter() : override the Yz_Class
     *  @todo check index
     *  @return this
     */
    protected function defaultSetter( $index, $value )
    {
        $this->__dict__[$index] = $value;
        return $this;
    }


    /** Render the included Javascripts (once and all at once)
     *  @return string
     */
    protected function renderJS()
    {
        debug();
        $o = "";
        // a non-topmost template SHOULD NOT render any JS
        foreach ($this->js as $jsId => $js)
            $o .= $this->doRenderJS($jsId, $js);

        // remove rendered js
        $this->js = array();
        return $o;
    }

    /** Render the included Styles (once and all at once)
     *  @return string
     */
    protected function renderCSS()
    {
        debug();
        $o = "";
        // a non-topmost template SHOULD NOT render any JS
        foreach ($this->css as $cssId => $css)
            $o .= $this->doRenderCSS($cssId, $css);

        // remove rendered css
        $this->css = array();
        return $o;
    }

    /** Render the assigned Metas (once and all at once)
     *  @return string
     */
    protected function renderMeta()
    {
        debug();
        $o = "";
        // a non-topmost template SHOULD NOT render any meta
        foreach ($this->meta as $name => $content) {
            $content = htmlspecialchars($content);
            $o .= "<meta name='{$name}' content='{$content}' />\n";
        }
        $this->meta = array();
        return $o;
    }


    /**
     *  @param string $name
     *  @param array $entry [0 => href, 1 => text]
     *  @return string
     */
    protected function doRenderCSS($href, $text)
    {
        debug("CSS '$href'");
        if (null === $text) {
            if ('./' === substr($href, 0, 2))
                $href = $this->__dict__['webRoot'] . substr($href, 2);

            $html = "<link rel='stylesheet' href='{$href}' type='text/css' />\n";
        } else {
            $html = <<<html
<style type="text/css" id="yz-style-{$href}" />
/* Inserted Style: '{$href}' */
{$text}
</style>\n
html;
        }
        return $html;
    }

    /**
     *  @param string $name
     *  @param array $entry [0 => src, 1 => text]
     *  @return string
     */
    protected function doRenderJS($src, $text)
    {
        debug("JS '$src'");
        if (null === $text) {
            if ('./' === substr($src, 0, 2))
                $src = $this->__dict__['webRoot'] . substr($src, 2);

            $src = " src='{$src}'";
            $text = "/* Inserted Javascript */";
        } else {
            $src = " id='yz-javascript-{$src}'";
            $text = "\n/* Inserted Javascript: {$src} <!-- */\n{$text}\n// -->\n";
        }
        $html = "<script type='text/javascript' {$src}>{$text}</script>\n";
        return $html;
    }

    /**
     *  @param Yz_Form_Interface $form
     *  @return string
     */
    protected function doRenderForm(Yz_Form_Interface $form)
    {
        debug();
        $html = $form->render($this);
        return $html;
    }

    /**
     *  @param Yz_Template_Interface $template
     *  @return string
     */
    protected function doRenderTemplate(Yz_Template_Interface $t)
    {
        debug();
        $t->__dict__ += $this->__dict__;
        $html = $t->render();
        $this->copyIncludesFrom($t);
        return $html;
    }

    protected function getI18N()
    {
        $i =& $this->i18n;
        if (null === $i)
            $i = $this->getModule()->getI18N();

        return $i;
    }

    protected function getModule()
    {
        return $this->module;
    }

    protected function copyIncludesFrom(Yz_Template_Interface $t)
    {
        $this->css  += $t->css;
        $this->js   += $t->js;
        $this->meta += $t->meta;
        return $this;
    }


    /** identify the template file and record the class which owns the file
     *  @param string|null
     *  @return Yz_File
     */
    protected function identifyFile()
    {
        $ownClass =& $this->ownerClass;
        $myPath =& $this->myPath;
        if (! $myPath) {
            // see next owner class up the hierarchy
            $ownClass = get_parent_class($ownClass);
            debug("Up class {$ownClass}");
            if (! is_subclass_of($ownClass, 'Yz_Template_Abstract'))
                throw new Yz_Template_Exception();

            // copy current ownerClass' template paths into this
            $myPath = $ownClass::$templatePath;
        }
        $entry = array_shift($myPath);
        $file = $this->makePath($entry, $ownClass);
        if ($file->isFile())
            return $file;

        // file not found -> next iteration
        return $this->identifyFile();
    }

    protected function makePath($pathStencil, $class)
    {
        $ns = $class::namespaceName();
        if ("" === $ns)
            throw new Yz_Template_Exception();

        list ($top, , $moduleName) = explode('\\', $ns);
        $fn = strtr($pathStencil, array(
            '%{site}' => Yz::getAppNamespace(),
            '%{modulePath}' => strtr($ns, '\\', '/'),
            '%{moduleName}' => lcfirst($moduleName),
            '%{name}' => lcfirst($class::shortClassName()),
            '%{type}' => $class::TEMPLATE_TYPE,
        ));
        return new Yz_File($fn, YZ_LIB);
    }


}
